/*
 * tpl_SafeQueue.hpp
 * Copyright (C) 2019 Alfredo Pons Menargues <apons@linucleus.com>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#ifndef __BASIC_TPL_SAFE_QUEUE_H__
#define __BASIC_TPL_SAFE_QUEUE_H__

#include <queue>
#include <list>
#include <mutex>
#include <thread>
#include <cstdint>
#include <condition_variable>
#include "basic/types.h"

namespace basic {

/** A thread-safe asynchronous queue */
template <class T, class Container = std::list<T>>
class tpl_SafeQueue {
  typedef typename Container::value_type value_type;
  typedef typename Container::size_type size_type;

public:
  /*! Create safe queue. */
  tpl_SafeQueue() = default;
  tpl_SafeQueue(tpl_SafeQueue&& sq) noexcept {
    m_queue = std::move(sq.m_queue);
  }
  tpl_SafeQueue(const tpl_SafeQueue& sq) {
    std::lock_guard<std::mutex> lock(sq.m_mutex);
    m_queue = sq.m_queue;
  }

  /*! Destroy safe queue. */
  ~tpl_SafeQueue() {
    std::lock_guard<std::mutex> lock(m_mutex);
  }

  bool Remove(const value_type& item) {
    std::lock_guard<std::mutex> lock(m_mutex);
    const int N{(int)m_queue.size()};
    for (int i = 0; i < N; ++i) {
      T v = m_queue.front();
      m_queue.pop();
      if (v->GetID() == item->GetID()) {
        return true;
      }
      m_queue.push(v);
    }
    return false;
  }
  /**
   *  Pushes the item into the queue.
   * \param[in] item An item.
   * \return true if an item was pushed into the queue
   */
  void Push(const value_type& item) {
    std::lock_guard<std::mutex> lock(m_mutex);
    m_queue.push(item);
    m_condition.notify_one();
  }

  /**
   *  Pushes the item into the queue.
   * \param[in] item An item.
   * \return true if an item was pushed into the queue
   */
  bool Push(const value_type&& item) {
    std::lock_guard<std::mutex> lock(m_mutex);
    m_queue.push(item);
    m_condition.notify_one();
    return true;
  }

  /**
   *  Pops item from the queue. If queue is empty, this function blocks until
   * item becomes available. \param[out] item The item.
   */
  void Pop(value_type& item) {
    std::unique_lock<std::mutex> lock(m_mutex);
    m_condition.wait(lock, [this]() // Lambda funct
                     { return !m_queue.empty(); });
    item = m_queue.front();
    m_queue.pop();
  }

  /**
   *  Pops item from the queue using the contained type's move assignment
   * operator, if it has one.. This method is identical to the pop() method if
   * that type has no move assignment operator. If queue is empty, this function
   * blocks until item becomes available. \param[out] item The item.
   */
  void MovePop(value_type& item) {
    std::unique_lock<std::mutex> lock(m_mutex);
    m_condition.wait(lock, [this]() // Lambda funct
                     { return !m_queue.empty(); });
    item = std::move(m_queue.front());
    m_queue.pop();
  }

  /**
   *  Tries to pop item from the queue.
   * \param[out] item The item.
   * \return False is returned if no item is available.
   */
  bool TryPop(value_type& item) {
    std::unique_lock<std::mutex> lock(m_mutex);

    if (m_queue.empty())
      return false;

    item = m_queue.front();
    m_queue.pop();
    return true;
  }

  /**
   *  Tries to pop item from the queue using the contained type's move
   * assignment operator, if it has one.. This method is identical to the
   * try_pop() method if that type has no move assignment operator. \param[out]
   * item The item. \return False is returned if no item is available.
   */
  bool TryMovePop(value_type& item) {
    std::unique_lock<std::mutex> lock(m_mutex);

    if (m_queue.empty())
      return false;

    item = std::move(m_queue.front());
    m_queue.pop();
    return true;
  }

  /**
   *  Pops item from the queue. If the queue is empty, blocks for timeout
   * microseconds, or until item becomes available. \param[out] t An item.
   * \param[in] timeout The number of microseconds to wait.
   * \return true if get an item from the queue, false if no item is received
   * before the timeout.
   */
  bool TimeoutPop(value_type& item, std::uint64_t timeout) {
    std::unique_lock<std::mutex> lock(m_mutex);

    if (m_queue.empty()) {
      if (timeout == 0)
        return false;

      if (m_condition.wait_for(lock, std::chrono::microseconds(timeout)) == std::cv_status::timeout)
        return false;
    }

    item = m_queue.front();
    m_queue.pop();
    return true;
  }

  /**
   *  Pops item from the queue using the contained type's move assignment
   * operator, if it has one.. If the queue is empty, blocks for timeout
   * microseconds, or until item becomes available. This method is identical to
   * the try_pop() method if that type has no move assignment operator.
   * \param[out] t An item.
   * \param[in] timeout The number of microseconds to wait.
   * \return true if get an item from the queue, false if no item is received
   * before the timeout.
   */
  bool TimeoutMovePop(value_type& item, std::uint64_t timeout) {
    std::unique_lock<std::mutex> lock(m_mutex);

    if (m_queue.empty()) {
      if (timeout == 0)
        return false;

      if (m_condition.wait_for(lock, std::chrono::microseconds(timeout)) == std::cv_status::timeout)
        return false;
    }

    item = std::move(m_queue.front());
    m_queue.pop();
    return true;
  }

  /**
   *  Gets the number of items in the queue.
   * \return Number of items in the queue.
   */
  size_type Size() const {
    std::lock_guard<std::mutex> lock(m_mutex);
    return m_queue.size();
  }

  /**
   *  Check if the queue is empty.
   * \return true if queue is empty.
   */
  bool IsEmpty() const {
    std::lock_guard<std::mutex> lock(m_mutex);
    return m_queue.empty();
  }

  /**
   *  Swaps the contents.
   * \param[out] sq The tpl_SafeQueue to swap with 'this'.
   */
  void swap(tpl_SafeQueue& sq) {
    if (this != &sq) {
      std::lock_guard<std::mutex> lock1(m_mutex);
      std::lock_guard<std::mutex> lock2(sq.m_mutex);
      m_queue.swap(sq.m_queue);

      if (!m_queue.empty())
        m_condition.notify_all();

      if (!sq.m_queue.empty())
        sq.m_condition.notify_all();
    }
  }

  /*! The copy assignment operator */
  tpl_SafeQueue& operator=(const tpl_SafeQueue& sq) {
    if (this != &sq) {
      std::lock_guard<std::mutex> lock1(m_mutex);
      std::lock_guard<std::mutex> lock2(sq.m_mutex);
      std::queue<T, Container> temp{sq.m_queue};
      m_queue.swap(temp);

      if (!m_queue.empty())
        m_condition.notify_all();
    }

    return *this;
  }

  /*! The move assignment operator */
  tpl_SafeQueue& operator=(tpl_SafeQueue&& sq) {
    std::lock_guard<std::mutex> lock(m_mutex);
    m_queue = std::move(sq.m_queue);

    if (!m_queue.empty())
      m_condition.notify_all();

    return *this;
  }

private:
  std::queue<T, Container> m_queue;
  mutable std::mutex m_mutex;
  std::condition_variable m_condition;
};

/*! Swaps the contents of two tpl_SafeQueue objects. */
template <class T, class Container>
void swap(tpl_SafeQueue<T, Container>& q1, tpl_SafeQueue<T, Container>& q2) {
  q1.swap(q2);
}
} // namespace basic

#endif
